Cимволы и Строки

Символы

  • Символы в c# всегда представлены 16-разрядным числом в формате юникод

Тип System.String

  • String наследуется напрямую от object

Для создания литерала строки в IL коде есть специальная команда ldstr, вместо обычного создания объекта через команду newobj

==Важно!
На момент компиляции, все литералы строк попадают в метаданные.
==

  • так как в различных системах перенос строки задается разными символами, рекомендуется использовать конструкцию Environment.NewLine
  • Если оператор + применяется только для литералов, то на момент компиляции литерал сольется в один и пойдет в метаданные.

Неизменяемые строки

Минусы:

  1. При частой работе со строками, нагружаем сборщик мусора.

Плюсы:

  1. Потокобезопасность, любые изменения породят новую строку, не будет гонок.
  2. Благодаря этому несколько ссылок могут указывать на один объект в памяти, не боясь что он измениться, что уменьшает занимаемую память.

По соображением производительности тип стринг слишком сильно интегрирован в CLR. В частности CLR точно знает расположение полей в этом типе и обращается к ним напрямую. По этой причине, string пришлось закрыть от наследования sealed . (Иначе бы, если бы люди могли наследоваться, они бы могли добавлять поля, а значит CLR бы перестраивал порядок полей и все бы падало)

Сравнение строк.

Рихтер крайне рекомендует использовать для сравнения следующие методы:
(тк их поведение предсказуемо)

  • Equals.
  • Compare
  • StartsWith
  • EndsWith

Чтобы менять, игнорировать регистр или нет, можно передавать параметр comparationType. Этот параметр принимает енамчик и в нем есть разные типы сравнений. Также существуют другие параметры включающие другие нюансы сравнения и они могут изменяться через параметр CompareOption (если есть)

Обрати внимание, если мы сравниваем с игнорированием регистра то лучше приводить обе строки к верхнему регистру, т.к. майскрософт оптимизировал сравнение строк только в верхнем регистре. (Если передавать флаг игнорирования регистра, то он под капотом так и сделает, просто приведет к верхнему)

Стоит следить за спец символами из левых языков если при сравнении включаешь флаг cultureInfo, т.к. шарпы подтягивают региональные стандарты языка на компе и могут считать одним и тем же например немецкую букву бетта и два символа ss (видимо у них на клавиатуре для ввода бетты пишут дважды s, хер знает, но такое бывает)

Интернирование строк

Если ты ожидаешь что будет много одинаковых строк в процессе работы приложения, то нужно применять интернирование строк.

Алгоритм работы Интернирования в общем:

  1. При запуске приложения, CLR создает в управляемой куче отдельный объект хэштаблицы. Эта таблица в качестве ключа использует текст внутри строки, а в качестве значения ссылку на объект этой строки в той же управляемой куче.
  2. Для приложения в классе String определены методы Intern() и IsInterned() которые могут добавлять или проверять наличие какой либо строки в этой хэш таблице.
  3. Метод Intern() проверяет, есть ли такая строка в хэш таблице, если есть, то возвращает ссылку на нее. Если нет, он создает новый объект в управляемой куче со значением этой строки, добавляет ее в хэш таблицу и возвращает ссылку на новый объект из управляемой кучи.
  4. IsInterned() возвращает ссылку на строку как собрат, а если строку не нашел, то налл.

Следует обратить внимание, что объект таблицы будет убит и за ним придет сборщик, только в случае, если приложение будет остановлено. А значит все строки которые были туда добавлены не будут убираться сборщиком. Этим и достигается то, что интернированые однажды строки будут всегда жить и их можно будет переиспользовать в других местах.

По умолчанию, все литералы строк, которые находятся в методанных, интернируются при запуске приложения. Однако, существует атрибут сборки который это запрещает

[assembly: System.Runtime.CompilerServices.CompilationRelaxations(CompilationRelaxations.NoStringInterning)]

, более того, он всегда ставится при комиляции, однако, clr его не слушается и все равно проводит интернирование, НО, стоит помнить, что он не обязан его делать, поэтому строить логику на том, что интернирование произойдет автоматически - опасно.

Пример использования интернирования:

private static int32 SearchWord(String word, String[] wordList){
	Int32 count = 0;
	
	for (Int32 i = 0; i < wordList.Length; i++){
		if(wordList[i].Equals(word)){
			count++;
		}
	}
	
	return count;
}

В этом примере clr придется на каждое слово из массива провдить посимвольное сравнение, при том, что слова в этом массиве могут повторяться, это долго.

private static int32 SearchWord(String word, String[] wordList){
// подразумевается, что все слова в листе уже интернированы.
	Int32 count = 0;
	
	word = String.Intern(word);
	
	for (Int32 i = 0; i < wordList.Length; i++){
		if(Object.ReferenceEquals(wordList[i], word)){
			count++;
		}
	}
	
	return count;
}

В таком случае он будет сравнивать по ссылке, что быстрее чем перебор каждого символа в каждой строке списка.

Оператор ==

Пока я разбирался в нем, я согласился с Рихтером что нахуй он вообще нужен.

Алгоритм:

  1. Проверяет, интернированы ли строки, если обе строки интернированы, то сраниваем по ссылке.
  2. Если хоть одна строка не интернирована, то циклом сравниваем посимвольно.

Для сранвния именно ссылок можно применять статический метод Object.ReferenceEquals()

Span и Memory

В версиях дотнета начиная с 7 были добавлены эти структуры, они являются оберткой над указателями в безопасном коде. Span применяется в рамках одного метода и не может передаваться, а Memory может передаваться. Они используются чтобы не пересоздавать строку или массив если хочешь работаь только с её частью.

“image.png” could not be found.

Так, можно будет работать только с кусочком строки, не создавая подстроку.

Для строки AsSpan возвращает только ридонли, так что менять не получиться

Также был добавлен конструкор

string(Span<Char>)

Также был добавлен сахар, такой, что теперь можно пистаь так

var str = string(”literal”);

Прочие операции со строками

Для того чтобы создать еще одну ссылку на существующую строку есть два варианта

  1. var newLink = oldLink

  2. var newLink = oldLink.Clone()

Для создания нового экземпляра полностью, есть два варианта

  1. var newString = new string(oldLink)
  2. (устаревший) var newString = String.Copy(oldLink)

StringBuilder

  • Имеет свойство maxCapacity, которое указывает на максимально доступный размер строки.
  • Имеет свойство Capacity, которое как и у всех.
  • По индексатору можно менять отдельные символы.

Под капотом имеет динамический массив символов чар. Стоит помнить, что при расширении массива происходит полное перекопированние элементов в расширенный массив

SecureString

В шарпе можно использовать класс который является АПИ для взаимодействия с неуправляемой кучей, это нужно например для работы с паролями, так как в управляемой куче пароли даже после удаления объекта могут долго висеть до сборки мусора, и их можно будет прочитать другим неуправляемым кодом
SecureString предоставляет возможность читать и добавлять символы в конец строки, шифрует символы, а также поддерживает IDispose
Однако, его использование считает устаревшим, так как он сильно усложняет код, сейчас гпт рекомендует просто хранить символы пароля в массиве на стеке.